from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from .task_runtime import TaskRuntime


DEFAULT_PLAN_RUNTIME_PATH = Path('.port_sessions') / 'plan_runtime.json'
VALID_PLAN_STATUSES = (
    'pending',
    'in_progress',
    'completed',
    'blocked',
    'cancelled',
)


@dataclass(frozen=True)
class PlanStep:
    step: str
    status: str = 'pending'
    task_id: str | None = None
    description: str | None = None
    priority: str | None = None
    active_form: str | None = None
    owner: str | None = None
    depends_on: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, Any]:
        return {
            'step': self.step,
            'status': self.status,
            'task_id': self.task_id,
            'description': self.description,
            'priority': self.priority,
            'active_form': self.active_form,
            'owner': self.owner,
            'depends_on': list(self.depends_on),
        }

    @classmethod
    def from_dict(cls, payload: dict[str, Any]) -> 'PlanStep':
        return cls(
            step=str(payload.get('step') or ''),
            status=_normalize_plan_status(payload.get('status')),
            task_id=(
                str(payload.get('task_id'))
                if isinstance(payload.get('task_id'), str) and payload.get('task_id')
                else None
            ),
            description=(
                str(payload.get('description'))
                if isinstance(payload.get('description'), str)
                and payload.get('description').strip()
                else None
            ),
            priority=(
                str(payload.get('priority'))
                if isinstance(payload.get('priority'), str)
                and payload.get('priority').strip()
                else None
            ),
            active_form=(
                str(payload.get('active_form'))
                if isinstance(payload.get('active_form'), str)
                and payload.get('active_form').strip()
                else None
            ),
            owner=(
                str(payload.get('owner'))
                if isinstance(payload.get('owner'), str)
                and payload.get('owner').strip()
                else None
            ),
            depends_on=_normalize_id_list(payload.get('depends_on')),
        )


@dataclass(frozen=True)
class PlanMutation:
    explanation: str | None
    store_path: str
    before_sha256: str | None
    after_sha256: str
    before_preview: str | None
    after_preview: str
    before_count: int
    after_count: int
    synced_tasks: int = 0
    synced_task_store_path: str | None = None
    synced_task_sha256: str | None = None


@dataclass
class PlanRuntime:
    steps: tuple[PlanStep, ...] = field(default_factory=tuple)
    explanation: str | None = None
    updated_at: str | None = None
    storage_path: Path = field(default_factory=lambda: DEFAULT_PLAN_RUNTIME_PATH.resolve())

    @classmethod
    def from_workspace(cls, cwd: Path) -> 'PlanRuntime':
        storage_path = (cwd.resolve() / DEFAULT_PLAN_RUNTIME_PATH).resolve()
        if not storage_path.exists():
            return cls(storage_path=storage_path)
        try:
            payload = json.loads(storage_path.read_text(encoding='utf-8'))
        except (OSError, json.JSONDecodeError):
            return cls(storage_path=storage_path)
        raw_steps = payload.get('steps')
        if not isinstance(raw_steps, list):
            return cls(storage_path=storage_path)
        steps: list[PlanStep] = []
        for item in raw_steps:
            if not isinstance(item, dict):
                continue
            step = PlanStep.from_dict(item)
            if step.step:
                steps.append(step)
        explanation = payload.get('explanation')
        updated_at = payload.get('updated_at')
        return cls(
            steps=tuple(steps),
            explanation=(
                explanation.strip()
                if isinstance(explanation, str) and explanation.strip()
                else None
            ),
            updated_at=str(updated_at) if isinstance(updated_at, str) and updated_at else None,
            storage_path=storage_path,
        )

    def update_plan(
        self,
        items: list[dict[str, Any]],
        *,
        explanation: str | None = None,
        task_runtime: TaskRuntime | None = None,
        sync_tasks: bool = True,
    ) -> PlanMutation:
        normalized_steps: list[PlanStep] = []
        for index, item in enumerate(items, start=1):
            if not isinstance(item, dict):
                continue
            step_text = item.get('step')
            if not isinstance(step_text, str) or not step_text.strip():
                continue
            task_id = item.get('task_id')
            normalized_steps.append(
                PlanStep(
                    step=step_text.strip(),
                    status=_normalize_plan_status(item.get('status')),
                    task_id=(
                        task_id.strip()
                        if isinstance(task_id, str) and task_id.strip()
                        else f'plan_{index}'
                    ),
                    description=(
                        item.get('description').strip()
                        if isinstance(item.get('description'), str)
                        and item.get('description').strip()
                        else None
                    ),
                    priority=(
                        item.get('priority').strip()
                        if isinstance(item.get('priority'), str)
                        and item.get('priority').strip()
                        else None
                    ),
                    active_form=(
                        item.get('active_form').strip()
                        if isinstance(item.get('active_form'), str)
                        and item.get('active_form').strip()
                        else None
                    ),
                    owner=(
                        item.get('owner').strip()
                        if isinstance(item.get('owner'), str)
                        and item.get('owner').strip()
                        else None
                    ),
                    depends_on=_normalize_id_list(item.get('depends_on')),
                )
            )
        mutation = self._persist(
            tuple(normalized_steps),
            explanation=(
                explanation.strip()
                if isinstance(explanation, str) and explanation.strip()
                else None
            ),
        )
        if sync_tasks and task_runtime is not None:
            dependents: dict[str, list[str]] = {}
            for step in self.steps:
                for dependency in step.depends_on:
                    dependents.setdefault(dependency, []).append(step.task_id or '')
            task_items = [
                {
                    'task_id': step.task_id or f'plan_{index}',
                    'title': step.step,
                    'description': step.description,
                    'status': _plan_status_to_task_status(step.status),
                    'priority': step.priority,
                    'active_form': step.active_form,
                    'owner': step.owner,
                    'blocked_by': list(step.depends_on),
                    'blocks': sorted(
                        dependency
                        for dependency in dependents.get(step.task_id or f'plan_{index}', [])
                        if dependency
                    ),
                }
                for index, step in enumerate(self.steps, start=1)
            ]
            task_mutation = task_runtime.replace_tasks(task_items)
            return PlanMutation(
                explanation=mutation.explanation,
                store_path=mutation.store_path,
                before_sha256=mutation.before_sha256,
                after_sha256=mutation.after_sha256,
                before_preview=mutation.before_preview,
                after_preview=mutation.after_preview,
                before_count=mutation.before_count,
                after_count=mutation.after_count,
                synced_tasks=task_mutation.after_count,
                synced_task_store_path=task_mutation.store_path,
                synced_task_sha256=task_mutation.after_sha256,
            )
        return mutation

    def clear_plan(self, *, task_runtime: TaskRuntime | None = None) -> PlanMutation:
        mutation = self._persist((), explanation=None)
        if task_runtime is not None:
            task_mutation = task_runtime.replace_tasks([])
            return PlanMutation(
                explanation=None,
                store_path=mutation.store_path,
                before_sha256=mutation.before_sha256,
                after_sha256=mutation.after_sha256,
                before_preview=mutation.before_preview,
                after_preview=mutation.after_preview,
                before_count=mutation.before_count,
                after_count=mutation.after_count,
                synced_tasks=task_mutation.after_count,
                synced_task_store_path=task_mutation.store_path,
                synced_task_sha256=task_mutation.after_sha256,
            )
        return mutation

    def render_summary(self) -> str:
        lines = [
            f'Local plan runtime file: {self.storage_path}',
            f'Total plan steps: {len(self.steps)}',
        ]
        if self.explanation:
            lines.append(f'- Explanation: {self.explanation}')
        counts: dict[str, int] = {}
        for step in self.steps:
            counts[step.status] = counts.get(step.status, 0) + 1
        if counts:
            lines.append(
                '- Status counts: '
                + ', '.join(f'{name}={count}' for name, count in sorted(counts.items()))
            )
        if self.updated_at:
            lines.append(f'- Updated: {self.updated_at}')
        return '\n'.join(lines)

    def render_plan(self) -> str:
        if not self.steps:
            return '# Plan\n\nNo stored plan is currently available.'
        lines = ['# Plan', '']
        if self.explanation:
            lines.extend(['## Explanation', self.explanation, ''])
        lines.append('## Steps')
        for index, step in enumerate(self.steps, start=1):
            details = [f'{index}. {step.step}', f'status={step.status}']
            if step.task_id:
                details.append(f'task_id={step.task_id}')
            if step.priority:
                details.append(f'priority={step.priority}')
            lines.append('- ' + '; '.join(details))
            if step.description:
                lines.append(f'  description: {step.description}')
            if step.active_form:
                lines.append(f'  active_form: {step.active_form}')
            if step.owner:
                lines.append(f'  owner: {step.owner}')
            if step.depends_on:
                lines.append(f"  depends_on: {', '.join(step.depends_on)}")
        return '\n'.join(lines)

    def _persist(
        self,
        steps: tuple[PlanStep, ...],
        *,
        explanation: str | None,
    ) -> PlanMutation:
        before_count = len(self.steps)
        before_text = self._serialize_payload(self.steps, self.explanation, self.updated_at)
        before_preview = _snapshot_text(before_text)
        before_sha256 = (
            hashlib.sha256(before_text.encode('utf-8')).hexdigest()
            if self.storage_path.exists() or self.steps or self.explanation
            else None
        )
        updated_at = datetime.now(timezone.utc).isoformat()
        payload_text = self._serialize_payload(steps, explanation, updated_at)
        self.storage_path.parent.mkdir(parents=True, exist_ok=True)
        self.storage_path.write_text(payload_text, encoding='utf-8')
        self.steps = steps
        self.explanation = explanation
        self.updated_at = updated_at
        after_sha256 = hashlib.sha256(payload_text.encode('utf-8')).hexdigest()
        return PlanMutation(
            explanation=explanation,
            store_path=str(self.storage_path),
            before_sha256=before_sha256,
            after_sha256=after_sha256,
            before_preview=before_preview if before_text.strip() else None,
            after_preview=_snapshot_text(payload_text),
            before_count=before_count if before_text.strip() else 0,
            after_count=len(steps),
        )

    def _serialize_payload(
        self,
        steps: tuple[PlanStep, ...],
        explanation: str | None,
        updated_at: str | None,
    ) -> str:
        payload = {
            'explanation': explanation,
            'updated_at': updated_at,
            'steps': [step.to_dict() for step in steps],
        }
        return json.dumps(payload, ensure_ascii=True, indent=2)


def _normalize_plan_status(value: Any) -> str:
    if isinstance(value, str):
        lowered = value.strip().lower()
        aliases = {
            'todo': 'pending',
            'open': 'pending',
            'done': 'completed',
            'complete': 'completed',
            'in-progress': 'in_progress',
            'in progress': 'in_progress',
            'blocked_on': 'blocked',
        }
        lowered = aliases.get(lowered, lowered)
        if lowered in VALID_PLAN_STATUSES:
            return lowered
    return 'pending'


def _plan_status_to_task_status(status: str) -> str:
    if status == 'completed':
        return 'completed'
    if status == 'in_progress':
        return 'in_progress'
    if status == 'blocked':
        return 'blocked'
    if status == 'cancelled':
        return 'cancelled'
    return 'pending'


def _normalize_id_list(value: Any) -> tuple[str, ...]:
    if not isinstance(value, list):
        return ()
    normalized: list[str] = []
    seen: set[str] = set()
    for item in value:
        if not isinstance(item, str):
            continue
        text = item.strip()
        if not text or text in seen:
            continue
        normalized.append(text)
        seen.add(text)
    return tuple(normalized)


def _snapshot_text(text: str, limit: int = 240) -> str:
    normalized = ' '.join(text.split())
    if len(normalized) <= limit:
        return normalized
    return normalized[: limit - 3] + '...'
