from __future__ import annotations

import os
from pathlib import Path
from typing import TYPE_CHECKING

from vibelexity.circular_deps.models import Cycle, DependencyGraph, ImportInfo
from vibelexity.circular_deps.parsers.go import GoImportParser
from vibelexity.circular_deps.parsers.python import PythonImportParser
from vibelexity.circular_deps.parsers.typescript import TypeScriptImportParser
from vibelexity.circular_deps.resolvers.go import GoPathResolver
from vibelexity.circular_deps.resolvers.python import PythonPathResolver
from vibelexity.circular_deps.resolvers.typescript import TypeScriptPathResolver

if TYPE_CHECKING:
    from vibelexity.circular_deps.models import Cycle
    from vibelexity.circular_deps.parsers.base import ImportParser
    from vibelexity.circular_deps.resolvers.base import PathResolver

_PY_EXTENSIONS = {".py"}
_TS_EXTENSIONS = {".ts", ".tsx", ".js", ".jsx"}
_GO_EXTENSIONS = {".go"}
_UNSUPPORTED_EXTENSIONS = {".json", ".md", ".txt"}


def _detect_language(filepath: str | Path) -> str | None:
    filepath_str = str(filepath)
    ext = os.path.splitext(filepath_str)[1].lower()
    if ext in _PY_EXTENSIONS:
        return "python"
    elif ext in _TS_EXTENSIONS:
        return "typescript"
    elif ext in _GO_EXTENSIONS:
        return "go"
    return None


def _get_parser(lang: str | None) -> ImportParser | None:
    if lang == "python":
        return PythonImportParser()
    elif lang == "typescript":
        return TypeScriptImportParser()
    elif lang == "go":
        return GoImportParser()
    return None


def _get_resolver(lang: str | None, config: dict | None = None) -> PathResolver | None:
    module_resolution: str | None = None
    if config:
        module_resolution = config.get("ts_module_resolution")

    if lang == "python":
        return PythonPathResolver()
    elif lang == "typescript":
        return TypeScriptPathResolver(
            module_resolution=module_resolution, filepath=None
        )
    elif lang == "go":
        return GoPathResolver()
    return None


def _find_common_root(files: list[str] | list[Path]) -> str:
    if not files:
        return ""

    abs_files = [os.path.abspath(str(f)) for f in files]

    if len(abs_files) == 1:
        return os.path.dirname(abs_files[0])

    path_parts = [p.split(os.sep) for p in abs_files]

    common_prefix: list[str] = []
    min_len = min(len(p) for p in path_parts)

    for i in range(min_len):
        part = path_parts[0][i]
        if all(p[i] == part for p in path_parts):
            common_prefix.append(part)
        else:
            break

    if not common_prefix:
        return os.sep

    return os.sep.join(common_prefix)


def build_dependency_graph(
    files: list[Path] | list[str], config: dict | None = None
) -> DependencyGraph:
    abs_files = [os.path.abspath(str(f)) for f in files]
    edges: dict[str, list[ImportInfo]] = {f: [] for f in abs_files}
    local_files = set(abs_files)

    if not files:
        return DependencyGraph({}, local_files, "")
    root_dir = _find_common_root(abs_files)

    for abs_file in abs_files:
        lang = _detect_language(Path(abs_file))
        if lang is None:
            continue

        ext = os.path.splitext(abs_file)[1].lower()
        if ext in _UNSUPPORTED_EXTENSIONS:
            continue

        parser = _get_parser(lang)
        resolver = _get_resolver(lang, config=config)

        if parser is None or resolver is None:
            continue

        try:
            source = Path(abs_file).read_text()
        except Exception:
            continue

        raw_imports = parser.extract_imports(source, str(abs_file))

        for imp in raw_imports:
            imp.resolved_path = resolver.resolve(
                imp.raw_module, str(abs_file), root_dir
            )

            if imp.resolved_path is not None and imp.resolved_path in local_files:
                edges[str(abs_file)].append(imp)

    return DependencyGraph(edges, local_files, root_dir)


def find_cycles(
    files: list[Path] | list[str], config: dict | None = None
) -> list[Cycle]:
    graph = build_dependency_graph(files)

    colors = {f: "WHITE" for f in graph.local_files}
    cycles: list[Cycle] = []

    def dfs(file: str, path: list, edges_in_path: list) -> None:
        colors[file] = "GRAY"
        path.append(file)

        for import_info in graph.edges.get(file, []):
            target = import_info.resolved_path
            if target is None or target not in graph.local_files:
                continue

            edges_in_path.append((file, target, import_info.line))

            if colors[target] == "GRAY":
                cycle_start = path.index(target)
                cycle_edges = _extract_cycle_edges(
                    edges_in_path, cycle_start, len(path)
                )
                cycle = Cycle(
                    files=path[cycle_start:] + [target],
                    edges=cycle_edges,
                    depth=len(path[cycle_start:]),
                )
                cycles.append(cycle)
            elif colors[target] == "WHITE":
                dfs(target, path.copy(), edges_in_path.copy())

        colors[file] = "BLACK"

    for file in sorted(graph.local_files):
        if colors[file] == "WHITE":
            dfs(file, [], [])

    unique_cycles: list[Cycle] = []
    seen: set = set()
    for cycle in cycles:
        cycle_key = tuple(sorted(cycle.files))
        if cycle_key not in seen:
            seen.add(cycle_key)
            unique_cycles.append(cycle)

    return unique_cycles


def _extract_cycle_edges(edges_in_path: list, cycle_start: int, path_len: int) -> list:
    return edges_in_path[cycle_start:]


def _find_strongly_connected_components(graph: DependencyGraph) -> list[list[str]]:
    index = 0
    indices: dict[str, int] = {}
    lowlink: dict[str, int] = {}
    stack: list[str] = []
    on_stack: set[str] = set()
    sccs: list[list[str]] = []

    def strongconnect(node: str) -> None:
        nonlocal index
        indices[node] = index
        lowlink[node] = index
        index += 1
        stack.append(node)
        on_stack.add(node)

        for import_info in graph.edges.get(node, []):
            target = import_info.resolved_path
            if target is None or target not in graph.local_files:
                continue

            if target not in indices:
                strongconnect(target)
                lowlink[node] = min(lowlink[node], lowlink[target])
            elif target in on_stack:
                lowlink[node] = min(lowlink[node], indices[target])

        if lowlink[node] == indices[node]:
            scc = []
            while True:
                popped = stack.pop()
                on_stack.remove(popped)
                scc.append(popped)
                if popped == node:
                    break
            sccs.append(scc)

    for node in sorted(graph.local_files):
        if node not in indices:
            strongconnect(node)

    return sccs


def _find_cycles_in_scc(scc: list[str], graph: DependencyGraph) -> list[Cycle]:
    scc_set = set(scc)
    subgraph_edges = {
        node: [imp for imp in graph.edges.get(node, []) if imp.resolved_path in scc_set]
        for node in scc
    }
    colors = {node: "WHITE" for node in scc}
    cycles: list[Cycle] = []

    def dfs_scc(node: str, path: list[str], edges_in_path: list) -> None:
        colors[node] = "GRAY"
        path.append(node)

        for imp in subgraph_edges.get(node, []):
            target = imp.resolved_path
            if target is None:
                continue

            edges_in_path.append((node, target, imp.line))

            if colors[target] == "GRAY":
                cycle_start = path.index(target)
                cycle = Cycle(
                    files=path[cycle_start:] + [target],
                    edges=edges_in_path[cycle_start:],
                    depth=len(path[cycle_start:]),
                )
                cycles.append(cycle)
            elif colors[target] == "WHITE":
                dfs_scc(target, path.copy(), edges_in_path.copy())

        colors[node] = "BLACK"

    for node in sorted(scc):
        if colors[node] == "WHITE":
            dfs_scc(node, [], [])

    return cycles


def find_cycles_tarjan(
    files: list[Path] | list[str], config: dict | None = None
) -> list[Cycle]:
    graph = build_dependency_graph(files)
    sccs = _find_strongly_connected_components(graph)

    cycles = []
    for scc in sccs:
        if len(scc) > 1:
            cycles.extend(_find_cycles_in_scc(scc, graph))

    unique_cycles = []
    seen = set()
    for cycle in cycles:
        cycle_key = tuple(sorted(cycle.files))
        if cycle_key not in seen:
            seen.add(cycle_key)
            unique_cycles.append(cycle)

    return unique_cycles
